#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
@Author   : long.hai
@Contact  : 1982964634@qq.com
"""
import asyncio
import json
import os
import re
import sys
import traceback
from uuid import uuid4
import requests
from playwright._impl._errors import TimeoutError, Error
# from skimage import io
# from skimage.transform import resize
# from skimage.metrics import structural_similarity as ssim
# import numpy as np
from playwright.async_api import Page
from _base.element import ElementAction
from _base.errors import AssertError, WaitTimeoutError, DOMError
from _base.session import SessionState
from _base.success import SuccessMessage as ss
from _step.step import LocatorList
from utils.base_request import BaseRequest
from utils.common import file_exists, upload_file_to_simple_drawing, upload_file_to_oss, compare
from utils.random_tool import my_faker


class PageAction(SessionState):

    def __init__(self, context, page, ws):
        self.context = context
        self.page: Page = page
        SessionState.__init__(self, ws)

    async def set_default_timeout(self, **kwargs):
        second = kwargs["params"]
        self.page.set_default_timeout(second)
        await self.log_info(f"设置页面默认超时时间 second： {second}")

    async def goto(self, **kwargs):
        """跳转到目标网址"""
        try:
            url = kwargs["params"]
            await self.log_info(f'即将跳转到目标网址 url: {url}')
            await self.page.goto(url, timeout=30000)
            await asyncio.sleep(0.5)
            await self.log_info(f"跳转成功，当前页面地址 url： {self.page.url}")
        except:
            raise WaitTimeoutError('WaitTimeoutError：等待页面加载超时。\n1.增加等待时间。\n2.检查网络连接和目标网址是否正常。')

    async def item_locator(self, locators: LocatorList):
        """根据多个定位获取元素"""
        try:
            element = None
            for by_item in locators.children:
                by_name = by_item.by
                params = by_item.params
                params = self.re_replace_variable(params)
                args = None
                kwargs = {}
                if by_name != "first":
                    if isinstance(params, str):
                        args = params
                    else:
                        for p_item in params:
                            if isinstance(p_item, str):
                                args = p_item
                            else:
                                key, value = p_item["key"], p_item["value"]
                                kwargs[key] = value
                    element = await getattr(ElementAction, by_name)(self.page, element, args, **kwargs)
            # 如果元素已经可见，调用 scroll_into_view_if_needed
            # scroll_into_view_if_needed 调用后，元素会处于稳定状态（Stable）
            # 详情见页面表格 https://playwright.dev/python/docs/actionability
            # if await element.count() == 1 and await element.is_visible():
            #     await element.scroll_into_view_if_needed()
            return element
        except Exception as e:
            raise e

    async def click(self, **kwargs):
        try:
            locators = kwargs["locators"]
            # print(kwargs)
            if locators:
                el = await self.item_locator(locators)
                # self.page.wait_for_load_state() is_en
                await el.click()
                # 使用re.search进行匹配
                str_el = await ss.get_str_selector(el)
                await self.log_info(f"点击选择器为: {str_el} 的元素")
            else:
                raise DOMError('元素定位语句不能为None或空。')
        except DOMError as e:
            raise e
        except WaitTimeoutError as e:
            raise e
        except Exception as e:
            traceback.print_exc()
            raise ValueError(f'page.click: {str(e)}')

    async def fill(self, **kwargs):
        try:
            locators, text = kwargs["locators"], kwargs["params"]
            el = await self.item_locator(locators)
            await el.click()
            await self.page.keyboard.press("Control+A")
            await el.fill(text)
            await self.log_info(f"输入值: {text}")
        except TimeoutError:
            raise WaitTimeoutError('等待元素出现时间超过5000ms')
        except Exception as e:
            raise e

    async def sleep(self, **kwargs):
        """固定等待时间"""
        second = kwargs["params"]
        if isinstance(second, str):
            second = eval(second)
        await asyncio.sleep(second)
        await self.log_info(f"固定等待时间: {second}秒。")

    async def element_wait_for(self, **kwargs):
        """元素等待参考 https://playwright.dev/python/docs/api/class-locator#locator-wait-for"""
        locators, att_name = kwargs["locators"], kwargs["params"]
        el = await self.item_locator(locators)
        await el.wait_for(state=att_name)

    async def get_current_url(self, **kwargs):
        """获取当前页面url"""
        var_name = kwargs["result"]
        self.case_pool[var_name] = await self.page.evaluate('window.location.href')

    async def re_extract(self, **kwargs):
        """正则表达式提取结果"""
        params, var_name = kwargs["params"], kwargs["result"]
        string, pattern = params.split(',')
        result = re.findall(rf'{pattern}', string)
        if len(result) > 0:
            self.case_pool[var_name] = result[0]
        else:
            self.case_pool[var_name] = ""

    async def get_element_text(self, **kwargs):
        """
        获取元素展示文本
        :param kwargs: locators: 元素定位列表, var_name: 变量名
        :return:
        """
        locators, var_name = kwargs["locators"], kwargs["result"]
        el = await self.item_locator(locators)
        if (await el.evaluate('e => e.tagName')).lower() == 'input':
            text = el.input_value()
        else:
            text = await el.text_content()
        self.case_pool[var_name] = text
        await self.log_info(f"获取元素展示文本: {text}")

    async def get_attribute(self, **kwargs):
        """
        获取元素属性值
        :param kwargs: locators: 元素定位列表, var_name: 变量名
        :return:
        """
        locators, att_name, var_name = kwargs["locators"], kwargs["params"], kwargs["result"]
        el = await self.item_locator(locators)
        att_value = await el.get_attribute(att_name)
        self.case_pool[var_name] = att_value
        await self.log_info(f"获取元素属性值: {att_value}")

    async def image_code(self, **kwargs):
        """
        解析base64图片验证码
        :param kwargs: 【script】js脚本、【var_name】缓存结果变量名
        :return:
        """
        try:
            script, var_name = kwargs["params"], kwargs["result"]
            evaluate_res = await self.page.evaluate(script)
            await self.log_info(f"操作: 执行js, script: {script}, result: {evaluate_res}")
            image_bs64 = evaluate_res.split(",")[1]
            await self.log_info(f"操作: 获取图片验证码bs64数据, bs64: {image_bs64}")
            code = await self.get_img_code(image_bs64)
            await self.log_info(f"操作: bs64转字符验证码, 验证码: {code}")
            self.case_pool[var_name] = code
        except Exception as e:
            raise e

    async def get_img_code(self, image_base64):
        try:
            url = "http://api.ttshitu.com/predict"
            data = {
                "username": "longyunhai",  # 图鉴用户名
                "password": "longhai123",  # 图鉴密码
                "typeid": 1003,
                "image": image_base64
            }
            res = requests.post(url=url, json=data)
            result = json.loads(res.text)
            await self.log_info(f"调用外部解析验证码: {str(result)}")
            if result["message"] == "success":
                img_code = result["data"]["result"]
                await self.log_info(f"解析图片验证码: {img_code}")
                return img_code
            else:
                await self.log_error("解析验证码失败")
                return None
        except Exception as e:
            raise e

    # 点击坐标 relative
    async def click_coordinate(self, **kwargs):
        try:
            position = kwargs["params"]
            position = position.split("\n")
            for item in position:
                p = item.split(",")
                x, y = int(p[0]), int(p[1])
                await self.page.mouse.click(x, y)
                await self.log_info(f"点击坐标, x: {x} y: {y}")
        except Exception as e:
            raise e

    async def click_relative_coordinate(self, **kwargs):
        try:
            locators, position = kwargs["locators"], kwargs["params"]
            el = await self.item_locator(locators)
            bounding_box = await el.bounding_box()
            position = position.split("\n")
            for item in position:
                p = item.split(",")
                x, y = int(p[0]) + bounding_box["x"], int(p[1]) + bounding_box["y"]
                await self.page.mouse.click(x, y)
                await self.log_info(f"点击坐标: x: {x}、y: {y}")
        except Exception as e:
            traceback.print_exc()
            raise e

    async def move_to_xy(self, **kwargs):
        position = kwargs["params"]
        p = position.split(",")
        x, y = int(p[0]), int(p[1])
        await self.page.mouse.move(x, y)
        await self.log_info(f"移动鼠标到指定坐标: x={x} y={y}")

    # ---------------------------------------------系统级别鼠标键盘操作----------------------------------------------------
    # 关闭打印弹窗
    async def close_print_window(self, **kwargs):
        pass

    async def reload(self, **kwargs):
        await self.page.reload()
        await self.log_info(f"重新加载页面。")

    # 鼠标操作
    async def move_to_element(self, **kwargs):
        """
        鼠标移动到元素上
        :param kwargs: locators:元素定位列表
        :return:
        """
        locators = kwargs["locators"]
        el = await self.item_locator(locators)
        await el.hover()
        await self.log_info(f"鼠标悬浮在元素上: {str(el)}")

    # 拖动元素，偏移指定距离
    async def drag_offset(self, **kwargs):
        """
        拖动元素，偏移指定距离
        :param kwargs: locator_list: 元素定位列表 x_offset, y_offset X和Y方向偏移距离
        :return:
        """
        locators = kwargs["locators"]
        el = await self.item_locator(locators)
        box = await el.bounding_box()
        x, y = box["x"] + box["width"] / 2, box["y"] + box["height"] / 2  # 元素中心点
        x_offset, y_offset = kwargs['params'].replace(' ', '').strip().split(',')  # 偏移量
        x_target, y_target = x + int(x_offset), y + int(y_offset)  # 目标坐标

        await self.page.mouse.move(x, y)
        await self.page.mouse.down()
        await self.page.mouse.move(x_target, y_target, steps=10)  # 设置分10次完成，太快会导致失败
        await self.page.mouse.up()

    async def send_upload_file(self, **kwargs):
        """
        上传文件
        :param kwargs: {locators: 元素定位器, params：{name: 文件名称, token: token}}
        """
        locators, file = kwargs["locators"], kwargs["params"]
        name, token = file["name"], file["token"]
        file_exists(name, token)
        file_path = f'files/{token}/{name}'
        if os.path.exists(file_path):
            el = await self.item_locator(locators)
            async with self.page.expect_file_chooser(timeout=5000) as fc_info:
                await el.click()  # 点击上传附件按钮
            file_chooser = await fc_info.value
            await file_chooser.set_files(file_path)

    async def get_numbers_in_element_text(self, **kwargs):
        """
        获取元素文本中的数字
        :param kwargs: {locator_list: 元素定位列表, index: 取结果中的下标元素, var_name: 接收变量名}
        :return:
        """
        locators, index, var_name = kwargs["locators"], kwargs["params"], kwargs["result"]
        el = await self.item_locator(locators)
        text = await el.text_content()
        pattern = r"\d+"
        res = re.findall(pattern, text)[index]
        await self.log_info(f"提取字符串中的数字: 字符串 {text}, 下标 {index}, 返回值 {res}")
        self.case_pool[var_name] = text

    # ---------------------------------------------键盘操作--------------------------------------------
    async def press(self, **kwargs):
        """
        操作键盘上的按键
        :param: kwargs: {params: 键盘上的某个按键}
        :return:
        """
        key = kwargs["params"]
        await self.page.keyboard.press(key)
        await self.log_info(f"模拟按下按键: {key}")

    #
    async def screenshot(self):
        """截图
        :return: 返回截图名称
        """
        img_name = ''
        try:
            await asyncio.sleep(0.5)
            img_name = str(uuid4())
            img_path = f"./screenshot/{img_name}.jpg"
            await self.page.screenshot(path=img_path)
            # 存到oss文件服务器
            img_data = upload_file_to_oss(img_path, img_name)
            if img_data["status"] == 1000:
                img_url = img_data["url"]
            else:
                # 如果上传失败则上传到作者（yunhai）腾讯云服务器
                upload_file_to_simple_drawing(img_path, img_name)
                if img_data["status"] == 1000:
                    img_url = img_data["url"]
                else:
                    img_url = None
            print("self.image_urls", self.image_urls)
            self.image_urls.append(img_url)
            # if os.path.exists(img_path):
            #     os.remove(img_path)
            # 存到mysql数据库的方法
            # img_bs64_data = img_gen_bs64(img_path)
            # Images.objects.create(uuid=img_name, data=img_bs64_data)
        except:
            raise ValueError("page.screenshot 截取屏幕异常。")
        finally:
            await self.log_info(f"截取屏幕并上传到oss, 文件名称: {img_name}")

    async def storage_state(self):
        """
        保存登录信息
        """
        try:
            await self.page.wait_for_timeout(2)
            alias = self.global_pool["alias"]
            # 登录成功后，获取当前页面的存储状态
            storage_state = self.context.storage_state()
            with open(f'./{alias}_storage_state.json', 'w') as file:
                json.dump(storage_state, file)
            await self.log_info(f"保存登录状态到文件: {alias}_storage_state.json")
            await self.page.wait_for_timeout(1)
        except Exception as e:
            await self.log_error(f"保存登录状态失败: {str(e)}")

    async def get_element_image(self, **kwargs):
        """
        获取元素的截图，并保存到 oss, 返回图片的 url
        :param kwargs: {locators: 元素定位列表, result: 缓存变量名}
        """
        locators, var_name = kwargs["locators"], kwargs["result"]
        el = await self.item_locator(locators)

        # 元素截图
        img_path = f"./screenshot/{str(uuid4())}.jpg"
        await el.screenshot(path=img_path)

        # 截图上传
        img_data = upload_file_to_oss(img_path, '')
        if img_data["status"] != 1000:
            return None

        # 删除截图
        if os.path.exists(img_path):
            os.remove(img_path)
        self.case_pool[var_name] = img_data["url"]
        return img_data["url"]

    # async def get_element_similarity(self, **kwargs):
    #     """
    #     获取图片的相似度，对照图片与当前元素图片
    #     :param kwargs: {locators: 元素定位列表, result: 缓存变量名}
    #     """
    #     locators, var_name = kwargs["locators"], kwargs["result"]
    #     old_image_url = kwargs["params"]
    #     new_image_url = await self.get_element_image(locators=locators, result='temp')
    #
    #     # 读取图片
    #     image1 = io.imread(old_image_url, as_gray=True)
    #     image2 = io.imread(new_image_url, as_gray=True)
    #
    #     # 调整图像大小以确保尺寸相同
    #     min_height = min(image1.shape[0], image2.shape[0])
    #     min_width = min(image1.shape[1], image2.shape[1])
    #     image1 = resize(image1, (min_height, min_width))
    #     image2 = resize(image2, (min_height, min_width))
    #
    #     # 计算结构相似性指数
    #     similarity_index, diff_image = ssim(image1, image2, full=True, data_range=1.0)
    #     self.case_pool[var_name] = similarity_index
    #
    #     # 保存差距图片，并上传
    #     img_path = f"./screenshot/diff-{str(uuid4())}.jpg"
    #     diff_image_normalized = (diff_image * 255).astype(np.uint8)
    #     io.imsave(img_path, diff_image_normalized)
    #     img_data = upload_file_to_oss(img_path, '')
    #     if img_data["status"] != 1000:
    #         return None
    #
    #     # 删除截图
    #     if os.path.exists(img_path):
    #         os.remove(img_path)
    #     diff_image_url = img_data['url']
    #
    #     print(f'图片相似度: {similarity_index}')
    #     print(f'old image url: {old_image_url}\nnew image url: {new_image_url}\ndiff image url: {diff_image_url}')

    async def get_attr_val(self, **kwargs):
        """获取元素属性值
        :param kwargs: { locators：定位值列表 }
        :return: 属性值
        """
        try:
            el = await self.item_locator(kwargs["locators"])
            att_value = await el.get_attribute(kwargs["att_name"])
            await self.log_info(f"获取元素属性值: {att_value}")
            return att_value
        except:
            raise ValueError("page.get_attr_val 获取元素属性值异常。")

    async def get_element_numbers(self, **kwargs):
        """获取元素个数
        :param kwargs: { locators：定位值列表 }
        :return: 元素个数
        """
        selectors = await self.page.query_selector_all(kwargs["locator"])
        return len(selectors)

    # 断言操作--------------------------------------------------------------------
    async def assert_element_exist(self, **kwargs):
        """断言元素是否存在
        :param kwargs: { locators：定位值列表 }
        """
        try:
            locators = kwargs["locators"]
            el = await self.item_locator(locators)
            assert await el.is_enabled()
            await self.log_info(f"断言成功，元素存在: {str(el)}")
        except:
            raise AssertError("断言失败，元素不存在")

    async def assert_element_not_exist(self, **kwargs):
        """断言元素是否不存在
        :param kwargs: { locators：定位值列表 }
        """
        try:
            locators = kwargs["locators"]
            el = await self.item_locator(locators)
            is_exist = el.is_enabled()
            assert not is_exist
            await self.log_info(f"断言成功，元素不存在: {str(el)}")
        except:
            raise AssertError("断言失败，元素存在")

    async def assert_element_attr_exist(self, **kwargs):
        """断言元素属性是否存在
        :param kwargs: {locators：定位值列表, att_name: 属性}
        :return:
        """
        try:
            el = await self.item_locator(kwargs["locators"])
            attr = await el.get_attribute(kwargs["att_name"])
            assert attr
            await self.log_info(f"断言通过，元素属性存在。 {attr}")
        except:
            raise AssertError("断言失败，元素属性不存在。")

    async def assert_element_is_hidden(self, **kwargs):
        """断言元素是否是隐藏
        :param kwargs: { locators：定位值列表 }
        """
        try:
            locators = kwargs["locators"]
            el = await self.item_locator(locators)
            assert el.is_hidden()
            await self.log_info(f"断言通过，元素不可见")
        except:
            raise AssertError("断言失败，元素可见")

    async def assert_two_value(self, **kwargs):
        """断言两个值的关系
        :param kwargs: { params：{ left： 左边值, right: 右边值, condition: 条件} }
        """
        expression_dict = kwargs["params"]
        try:
            left, right, condition = expression_dict["left"], expression_dict["right"], expression_dict["condition"]
            assert compare(left, condition, right)
            await self.log_info(f"断言定两个值的关系成功, {left} {right} {condition}")
        except:
            raise AssertError(f"断言两个值的关系失败：{expression_dict}")

    async def script(self, **kwargs):
        """执行js脚本
        :param kwargs: { params：js字符脚本, result: 返回值解释变量 }
        """
        try:
            script, var_name = kwargs["params"], kwargs["result"]
            result = await self.page.evaluate(script)
            await self.log_info(f"操作: 执行js, script: {script}, result: {result}")
            self.case_pool[var_name] = result
        except:
            raise ValueError("page.script 运行异常，请检查代码。")

    # 智能等待
    async def intelligent_wait(self, state):
        await self.page.wait_for_load_state(state)

    # ------------------------------------------监听方法------------------------------------------------

    def on_popup(self, popup):
        """监听新窗口事件"""
        self.log_info(f"New popup page opened: {popup.url}")

    async def handle_print_dialog(self, dialog):
        """监听dialog"""
        await dialog.accept()  # 点击确定按钮

    def on_url_change(self, url: str):
        """监听url变化"""
        self.log_info(f"监听url变化: {url}")

    async def run_http_request(self, **kwargs):
        """
        执行HTTP请求
        :return:
        """
        try:
            body = kwargs["params"]
            method, url, headers, json_data = body["method"], body["url"], body["headers"], body["json"]
            headers = {item["key"]: item["value"] for item in headers} if len(headers) > 0 else None  # 处理headers
            br = BaseRequest()
            response = br.request(method, url, headers=headers, json=json_data, extract_list=body["extract"])
            status_code, extract_result = response["status_code"], response["extract_result"]
            for k, v in extract_result.items():
                self.case_pool[k] = v
            await self.log_info(f"执行HTTP请求\nmethod: {method},\nurl: {url},\nstatus_code: {status_code}")
        except:
            raise ValueError("page.run_http_request 方法运行异常。")

    def re_replace_variable(self, dict_or_string):
        """变量替换方法
        :param dict_or_string: 被替换的字典或字符串
        :return:
        """
        try:
            # 判断传入参数是否为字典
            new_string = json.dumps(dict_or_string, ensure_ascii=False) if isinstance(dict_or_string, (dict, list)) else dict_or_string
            # 判断数据源是否包含$符号
            if "$" in new_string:
                # 替换全局变量
                if "$GBL{" in new_string:
                    # print(self.global_var_pool)
                    if re.search(r"\S*\$GBL\{(.*?)}\s*", new_string):
                        result = re.findall(r".*?\$GBL\{(.*?)}.*?", new_string)
                        for key in result:
                            if key in self.global_pool:
                                new_string = new_string.replace("$GBL{%s}" % (key,), str(self.global_pool[key]))

                # 替换环境变量
                if "$ENV{" in new_string:
                    if re.search(r"\S*\$ENV\{(.*?)}\s*", new_string):
                        result = re.findall(r".*?\$ENV\{(.*?)}.*?", new_string)
                        for key in result:
                            if key in self.env_pool:
                                new_string = new_string.replace("$ENV{%s}" % (key,), str(self.env_pool[key]))

                # 随机函数 ${__random_name()__}
                if "${__" in new_string:
                    if re.search(r"\S*\$\{__(.*?)}\s*", new_string):
                        result = re.findall(r".*?\$\{__(.*?)}.*?", new_string)
                        for key in result:
                            res = key.split('(')
                            args = eval(f"({res[1]}")
                            if args:
                                result_data = getattr(my_faker, res[0])(args)
                            else:
                                result_data = getattr(my_faker, res[0])()
                            new_string = new_string.replace("${__%s}" % (key,), result_data)

                # 替换局部变量
                if "${" in new_string:
                    # print(self.case_var_pool)
                    if re.search(r"\S*\$\{(.*?)}\s*", new_string):
                        result = re.findall(r".*?\$\{(.*?)}.*?", new_string)
                        for key in result:
                            new_string = new_string.replace("${%s}" % (key,), str(self.case_pool[key]))

            # 如果传入的dict字典数据，则转为字典返回，反之则返回字符串
            replace_result = json.loads(new_string) if isinstance(dict_or_string, (dict, list)) else new_string
            return replace_result
        except Exception as e:
            raise ValueError(f"变量替换处理异常：{str(e)}")

    async def check_response(self, response):
        """监听接口操作请求
        """
        if self.extract_state:
            print("self.extract_path", self.extract_state, self.extract_path)
            # response.finished()
            # print("URL:{}".format(response.request.url))
            # https://test.snkoudai.com/api/produce/co/certificate/generateCertificateCode
            if self.extract_path in response.request.url:
                # print("URL:{}".format(response.request.url))
                # print("method:{}".format(response.request.method))
                # print("status:{}".format(response.status))
                body = await response.json()
                # print("body:{}".format(body))
                # print("timing:{} 毫秒".format(response.request.timing['responseEnd']))
        # print("******************************监听接口相应操作***************************************")

    async def get_attr_exist(self, **kwargs):
        """
        判断元素是否存在
        :param kwargs: locators：定位值列表
        :return: True或False
        """
        el = await self.item_locator(kwargs["locators"])
        attr = await el.get_attribute(kwargs["att_name"])
        attr = True if attr else False
        await self.log_info(f"获取元素属性: {attr}")
        return attr
